查看原文
其他

告诉python,我想“狂飙”了——线程池与异步协程为爬虫提速

爬虫俱乐部 Stata and Python数据分析 2023-10-24

本文作者:罗天尧,新疆大学商学院

本文编辑:胡艺粼

技术总编:方一卓

Stata and Python 数据分析

爬虫俱乐部Stata基础课程Stata进阶课程Python课程可在小鹅通平台查看,欢迎大家多多支持订阅!如需了解详情,可以通过课程链接(https://appbqiqpzi66527.h5.xiaoeknow.com/homepage/10)或课程二维码进行访问哦~

许多资料整理者,深受网速的困扰。但或许这种困扰并不能完全归咎于网速,如果能充分调动计算机的“主观能动性”,在请求端我们将能够更高效地搜集数据。

一、思路
工欲善其事,必先利其器。为了掌握高效率的爬虫技术,我们首先要建立一个学习工具——爬虫程序。进入水果交易网站(https://www.guo68.com/),将页面切换到水果行情中。1.1 网页链接的翻页规律由简入繁,先从单个页面建立分析。将页面切换到第二页,可以发现网页的翻页动作,是通过https://www.guo68.com/market?page=2中的page所控制的。因此,对于翻页操作,我们可以通过修改url中的page参数,或是request的请求中携带参数完成(如下图所示)。这样我们便可以实现多页爬取。


1.2 尝试获取网页文本尝试获取页面中的静态内容。在python编译器中构造headers,对目标网页发起请求。
import requests
cookies = { 'PHPSESSID': 'juntbj9ejl8dnroa8qa4r7pbnu',}
headers = { 'Connection': 'keep-alive', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-User': '?1', 'Sec-Fetch-Dest': 'document', 'Referer': 'https://www.guo68.com/market?', 'Accept-Language': 'zh-CN,zh;q=0.9', # 'Cookie': 'PHPSESSID=juntbj9ejl8dnroa8qa4r7pbnu',}
resp = requests.get('https://www.guo68.com/market', cookies=cookies, headers=headers).textprint(resp)可以看到,返回的数据中包含了数据信息。同时,该网页的元素信息较为工整,我们可以使用xpath,借助标签特性来抓取内容。回到浏览器中,通过开发者工具,查看elements中的信息。通过元素选择工具,查询到所有数据详细信息,均工整保留在<ul class="market-list">标签中,说明这个网页就是为xpath“而生”的。右键复制完整的xpath路径:/html/body/div[3]/div/div[1]/div[3]/div[2]/ul[2]/a。接前文代码,进行测试:
from lxml import etreehtml = etree.HTML(resp) #建立解析对象table = html.xpath('/html/body/div[3]/div/div[1]/div[3]/div[2]/ul/a/li/text()') #注意这里需要手动微调,删去多余的索引值print(table)可以看到,数据信息已经被提取出来。二、建立爬虫程序至此,我们完成了单页请求的测试,可以进一步批量爬取数据,并将信息写入csv文件。本案例的测试都基于网站前200页完成。将上述代码整理为函数形式,方便调用:
def spider(url): resp = requests.get(url, cookies=cookies, headers=headers).text html = etree.HTML(resp) table = html.xpath('/html/body/div[3]/div/div[1]/div[3]/div[2]/ul/a') for tr in table : #table仍保留了xpath属性,我们可以借此生成工整的信息格式 txt = tr.xpath('./li/text()') #在此将每个<a>标签中的文字信息提取 csv.writerow(list(txt)) #将一组<a>标签中的信息以列表形式传入
if __name__ == '__main__': f = open('data.csv', encoding='utf-8', mode='w', newline='') #newline去除空行 csv = csv.writer(f) #提前打开为对象,避免反复打开文件耗时 start=time.time() #可以提前import time进行测试的计时操作 for i in range(1, 200): url=f'https://www.guo68.com/market?page={i}' spider(url) end=time.time() print(f"耗时:{end-start}s")执行代码并等待信息录入。
D:\Anaconda\python.exe "D:/pycharm project/test.py"耗时:47.112051486968994sProcess finished with exit code 0查看已经写入的csv文件。虽然我们爬取到了信息,但是速度不尽人意。那么有什么方式能够提升python的爬虫速度呢?三、“狂飙”测试直觉上,我们有两种思路来提升爬虫的速度:一是增加爬虫的设备,二是消除爬虫过程中的“无效等待时间”(即阻塞等待,前文代码运行逻辑为:爬取—写入—爬取—……)。这分别对应这计算机中的两种技术思路:多线程与协程。为了了解二者的逻辑与区别,我们先从进程聊起。作为爬虫使用者,我们可以只专注于技术,暂时忽略原理上的繁杂。3.1 线程池如下图所示,这是一台电脑CPU运行时的状态,我们可以将各个独立的程序(每个框中的程序)看做一个进程,他们可以保持“互不干扰”,也可以"相互交流"保持系统稳定。我们将视角放大到pycharm之中,类似CPU中的情形,下图是pycharm进程中的多个子进程(每个框中的程序)。从这个角度看,我们是否可以多开几个爬虫进程呢?答案是肯定的,但从爬虫角度来看,我们暂不需要多进程技术。一个更简洁的思路是,在一个进程中“套娃”——开启多个线程,即将多个任务(project)压缩在一个进程(project)中进行。可以对比前文的代码,线程的开辟是极为简单的,即:在程序上导入线程包;指定开启线程;提交任务;关闭线程。为了提高抓取速率,我们可以导入线程池。先编写主程序,开辟线程池为50,并通过循环将网页传入后面编写的函数中。
from concurrent.futures import ThreadPoolExecutor #导包
def spider(url): resp = requests.get(url, cookies=cookies, headers=headers).text html = etree.HTML(resp) table = html.xpath('/html/body/div[3]/div/div[1]/div[3]/div[2]/ul/a') for tr in table : txt = tr.xpath('./li/text()') csv.writerow(list(txt))
if __name__ == '__main__': f = open('data.csv', encoding='utf-8', mode='w', newline='') csv = csv.writer(f) start=time.time() with ThreadPoolExecutor(50) as t: #开辟线程 for i in range(1, 200): t.submit(spider, f'https://www.guo68.com/market?page={i}') #开始线程 end=time.time() print(f"耗时:{end-start}s") t.shutdown(wait= True) #关闭线程执行上述代码,我们的爬虫速度已经有了明显提升。
D:\Anaconda\python.exe "D:/pycharm project/test.py"耗时:13.467449502944946sProcess finished with exit code 0当然,线程池在数据传输上也会有所缺陷。关于此问题,或许你还要了解线程安全与互斥锁等。感兴趣的读者可以测试以下代码,此处不过多赘述。
#输出结果有随机数据污染(多刷新几次)from threading import Thread
def task(count: int): for n in range(count): print(n) thread1 = Thread(target=task, args=(10,))thread2 = Thread(target=task, args=(20,))
thread1.start()thread2.start()
thread1.join()thread2.join()
print("Main threads is end")3.2 异步协程
简单来看,单线程的爬虫就像一场接力赛,需要在爬取和存储直接切换。但显然,请求和写入是两个独立的动作,他们只是在“无为地等待生命中的下一次”。


我们试图将上图的逻辑,纠正为下图。异步协程的手段,更像是“二者在异地工作,却又为了一个目标协同并进”,二者同步进行,互不干扰,压缩了工作时间。在第二节程序的基础上,我们可以进行对比:异步协程使用的是aiohttp模块进行的请求操作;定义函数前要加async;在阻塞操作处添加await;在main函数中创建异步任务;最后以loop循环的方式调用。
import asyncioimport aiohttp
f = open('data.csv', encoding='utf-8', mode='w', newline='')c = csv.writer(f)url_list = [ f"https://www.guo68.com/market?page={x}" for x in range(1, 200)]
async def spider(url): async with aiohttp.ClientSession() as session: # 发送网络请求 async with session.get(url,cookies=cookies, headers=headers) as resp: resp = await resp.content.read() html = etree.HTML(resp) table = html.xpath('/html/body/div[3]/div/div[1]/div[3]/div[2]/ul/a') for tr in table: txt = tr.xpath('./li/text()') c.writerow(list(txt))
async def main(): tasks = [] for url in url_list: tasks.append(spider(url)) await asyncio.wait(tasks)
if __name__ == '__main__': start=time.time() # asyncio.run(main()) #RuntimeError: Event loop is closed loop = asyncio.get_event_loop() loop.run_until_complete(main()) end=time.time() print(f"耗时:{end-start}s")
D:\Anaconda\python.exe "D:/pycharm project/test.py"耗时:11.6561918258667sProcess finished with exit code 0观察爬取到的文件,由于异步的存在,我们爬取的数据顺序并不是固定的。读者也可以继续通过生产消费者模型,来理解这一现象。四、结语我们只是简要的介绍了两种技术的思想,帮助读者更好地理解二者的逻辑。实际操作中,还需要考虑资源消耗、实际响应速度等,需要更深入的爬虫技术学习。同时,在海量的测试中,异步协程技术往往是更为高效的。当然,我们还要考虑网络阻塞、服务器请求限制等问题,结合实际测试,选取最优的工具。有兴趣的读者,也可以尝试用time.sleep()来模拟网络和计算机的阻塞。END

重磅福利!为了更好地服务各位同学的研究,爬虫俱乐部将在小鹅通平台上持续提供金融研究所需要的各类指标,包括上市公司十大股东、股价崩盘、投资效率、融资约束、企业避税、分析师跟踪、净资产收益率、资产回报率、国际四大审计、托宾Q值、第一大股东持股比例、账面市值比、沪深A股上市公司研究常用控制变量等一系列深加工数据,基于各交易所信息披露的数据利用Stata在实现数据实时更新的同时还将不断上线更多的数据指标。我们以最前沿的数据处理技术、最好的服务质量、最大的诚意望能助力大家的研究工作!相关数据链接,请大家访问:(https://appbqiqpzi66527.h5.xiaoeknow.com/homepage/10)或扫描二维码:

最后,我们为大家揭秘雪球网(https://xueqiu.com/)最新所展示的沪深证券和港股关注人数增长Top10。



对我们的推文累计打赏超过1000元,我们即可给您开具发票,发票类别为“咨询费”。用心做事,不负您的支持!







往期推文推荐 高级函数——map()和reduce()

Stata绘制条形图的进阶用法

快来看看武汉的房价是不是又双叒叕涨了!Python 常见内置函数(二)

Stata绘制饼形图的进阶用法

Python标准库--logging模块盲区探索——Stata的读写极限Camelot提取PDF表格:一页多表、多页一表

Stata绘图系列——条形图绘制

Python常见内置函数(一)Stata绘图系列——饼形图绘制【爬虫实战】深交所服务业年报数据

“挂羊头卖狗肉”?

Python与excel交互--xlsxwriter模块

cnmapsearch——离公司最近的快餐店在哪

Python中的异常处理 Python交互式数据可视化——酷炫的Altair库 hk系列命令(3)—— hktrade
     关于我们 

   微信公众号“Stata and Python数据分析”分享实用的Stata、Python等软件的数据处理知识,欢迎转载、打赏。我们是由李春涛教授领导下的研究生及本科生组成的大数据处理和分析团队。

   武汉字符串数据科技有限公司一直为广大用户提供数据采集和分析的服务工作,如果您有这方面的需求,请发邮件到statatraining@163.com,或者直接联系我们的数据中台总工程司海涛先生,电话:18203668525,wechat: super4ht。海涛先生曾长期在香港大学从事研究工作,现为知名985大学的博士生,爬虫俱乐部网络爬虫技术和正则表达式的课程负责人。



此外,欢迎大家踊跃投稿,介绍一些关于Stata和Python的数据处理和分析技巧。

投稿邮箱:statatraining@163.com投稿要求:1)必须原创,禁止抄袭;2)必须准确,详细,有例子,有截图;注意事项:1)所有投稿都会经过本公众号运营团队成员的审核,审核通过才可录用,一经录用,会在该推文里为作者署名,并有赏金分成。2)邮件请注明投稿,邮件名称为“投稿+推文名称”。3)应广大读者要求,现开通有偿问答服务,如果大家遇到有关数据处理、分析等问题,可以在公众号中提出,只需支付少量赏金,我们会在后期的推文里给予解答。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存